﻿// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
using Resources = Microsoft.AspNetCore.Mvc.ViewFeatures.Resources;

namespace Microsoft.AspNetCore.Mvc
{
    public class PageRemoteAttributeTest
    {
        [Fact]
        public void GetUrl_CallsUrlHelperWithExpectedValues()
        {
            // Arrange
            var testableAttribute = new TestablePageRemoteAttribute
            {
                PageName = "Foo",
                PageHandler = "Bar"
            };

            var ambientValues = new RouteValueDictionary()
            {
                ["page"] = "/Foo"
            };

            var routeData = new RouteData(ambientValues)
            {
                Routers = { Mock.Of<IRouter>() }
            };

            var urlHelper = new MockUrlHelper(url: "/Foo?handler=Bar")
            {
                ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData)
            };

            var validationContext = GetValidationContext(urlHelper);

            // Act
            testableAttribute.InvokeGetUrl(validationContext);

            // Assert
            var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);

            Assert.Equal(2, routeDictionary.Count);
            Assert.Equal("/Foo", routeDictionary["page"] as string);
            Assert.Equal("Bar", routeDictionary["handler"] as string);
        }

        [Fact]
        public void GetUrl_WhenUrlHelperReturnsNull_Throws()
        {
            // Arrange
            var testableAttribute = new TestablePageRemoteAttribute
            {
                PageName = "Foo",
                PageHandler = "Bar"
            };

            var ambientValues = new RouteValueDictionary
            {
                ["page"] = "/Page"
            };

            var routeData = new RouteData(ambientValues)
            {
                Routers = { Mock.Of<IRouter>() }
            };
            
            var urlHelper = new MockUrlHelper(url: null)
            {
                ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData)
            };

            var validationContext = GetValidationContext(urlHelper);

            // Act && Assert
            ExceptionAssert.Throws<InvalidOperationException>(
                () => testableAttribute.InvokeGetUrl(validationContext),
                Resources.RemoteAttribute_NoUrlFound);
        }

        [Fact]
        public void GetUrl_WhenPageNameIsNotSet_WillUsePageNameFromAmbientValues()
        {
            // Arrange
            var testableAttribute = new TestablePageRemoteAttribute()
            {
                PageHandler = "Handler"
            };

            var ambientValues = new RouteValueDictionary
            {
                ["page"] = "/Page"
            };

            var routeData = new RouteData(ambientValues)
            {
                Routers = { Mock.Of<IRouter>() }
            };

            var urlHelper = new MockUrlHelper(url: "/Page?handler=Handler")
            {
                ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData)
            };

            var validationContext = GetValidationContext(urlHelper);

            // Act
            var actualUrl = testableAttribute.InvokeGetUrl(validationContext);

            // Assert
            Assert.Equal("/Page?handler=Handler", actualUrl);
        }

        [Fact]
        public void GetUrl_WhenPageNameAndPageHandlerIsNotSet_WillUseAmbientValues()
        {
            // Arrange
            var testableAttribute = new TestablePageRemoteAttribute();

            var ambientValues = new RouteValueDictionary
            {
                ["page"] = "/Page",
                ["handler"] = "Handler"
            };

            var routeData = new RouteData(ambientValues)
            {
                Routers = { Mock.Of<IRouter>() }
            };

            var urlHelper = new MockUrlHelper(url: "/Page?handler=Handler")
            {
                ActionContext = GetActionContext(new ServiceCollection().BuildServiceProvider(), routeData)
            };

            var validationContext = GetValidationContext(urlHelper);

            // Act
            var actualUrl = testableAttribute.InvokeGetUrl(validationContext);

            // Assert
            Assert.Equal("/Page?handler=Handler", actualUrl);
        }
        
        private static ClientModelValidationContext GetValidationContext(IUrlHelper urlHelper, RouteData routeData = null)
        {
            var serviceCollection = GetServiceCollection();
            var factory = new Mock<IUrlHelperFactory>(MockBehavior.Strict);
            serviceCollection.AddSingleton<IUrlHelperFactory>(factory.Object);

            var serviceProvider = serviceCollection.BuildServiceProvider();
            var actionContext = GetActionContext(serviceProvider, routeData);

            factory
                .Setup(f => f.GetUrlHelper(actionContext))
                .Returns(urlHelper);

            var metadataProvider = new EmptyModelMetadataProvider();
            var metadata = metadataProvider.GetMetadataForProperty(
                containerType: typeof(string),
                propertyName: nameof(string.Length));

            return new ClientModelValidationContext(
                actionContext,
                metadata,
                metadataProvider,
                new AttributeDictionary());
        }

        private static ServiceCollection GetServiceCollection()
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection
                .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
                .AddSingleton<ILoggerFactory>(new NullLoggerFactory());

            serviceCollection.AddOptions();
            serviceCollection.AddRouting();

            serviceCollection.AddSingleton<IInlineConstraintResolver>(
                provider => new DefaultInlineConstraintResolver(provider.GetRequiredService<IOptions<RouteOptions>>(), provider));

            return serviceCollection;
        }

        private static ActionContext GetActionContext(IServiceProvider serviceProvider, RouteData routeData)
        {
            // Set IServiceProvider properties because TemplateRoute gets services (e.g. an ILoggerFactory instance)
            // through the HttpContext.
            var httpContext = new DefaultHttpContext
            {
                RequestServices = serviceProvider,
            };

            if (routeData == null)
            {
                routeData = new RouteData
                {
                    Routers = { Mock.Of<IRouter>(), },
                };
            }

            return new ActionContext(httpContext, routeData, new ActionDescriptor());
        }

        private class TestablePageRemoteAttribute : PageRemoteAttribute
        {
            public string InvokeGetUrl(ClientModelValidationContext context)
            {
                return base.GetUrl(context);
            }
        }

        private class MockUrlHelper : IUrlHelper
        {
            private readonly string _url;

            public MockUrlHelper(string url)
            {
                _url = url;
            }

            public ActionContext ActionContext { get; set; }

            public object RouteValues { get; private set; }

            public string Action(UrlActionContext actionContext)
            {
                throw new NotImplementedException();
            }

            public string Content(string contentPath)
            {
                throw new NotImplementedException();
            }

            public bool IsLocalUrl(string url)
            {
                throw new NotImplementedException();
            }

            public string Link(string routeName, object values)
            {
                throw new NotImplementedException();
            }

            public string RouteUrl(UrlRouteContext routeContext)
            {
                RouteValues = routeContext.Values;

                return _url;
            }
        }
    }
}
